package net.cyndeline.scalarlib.rldungeon.dgs.strategy

import scalax.collection.GraphPredef.OuterEdge
import net.cyndeline.scalarlib.rldungeon.dgs.graph.DGSFunctionality
import scalax.collection.GraphEdge.UnDiEdge
import net.cyndeline.scalarlib.rldungeon.dgs.graph.Room
import net.cyndeline.scalarlib.rldungeon.dgs.{ActivatorResponderAmountEstimator, RewardProcessor, ActivatorResponderProcessor, Parameter}
import net.cyndeline.scalarlib.rlgraph.util.EdgeCopyFactory
import scala.util.Random
import net.cyndeline.scalarlib.rldungeon.grammar.Strategy
import net.cyndeline.scalarlib.rldungeon.grammar.production.GraphProduction
import net.cyndeline.scalarlib.rldungeon.dgs.strategy.hillclimbing.HillClimbing
import net.cyndeline.scalarlib.rldungeon.dgs.strategy.help.{DummyStrategy, ParameterAcceptRatio}
import net.cyndeline.scalarlib.rldungeon.util.ProbabilityCollection
import net.cyndeline.scalarlib.rldungeon.dgs.strategy.pointlessArea.{PointlessAreaStrategy, PointlessAreaReductionStrategy}
import scala.reflect.runtime.universe._
import scala.reflect._
import net.cyndeline.scalarlib.rldungeon.dgs.strategy.pointlessArea.help.DefaultActivatorResponderEstimator
import net.cyndeline.scalarlib.subcut.ProjectConfiguration

/**
 * Creates the strategy responsible for generating levels.
 *
 * @constructor Creates a new level-strategy builder.
 * @param edgeCopyFactory Factory that copies edges by replacing their vertices.
 * @param random Random object used to randomize the main strategies for the level, topology, object placement
 *               and pointless areas.
 * @param activatorResponderProcessor Factory that produces new vertex/edge pairs by adding responders and
 *                                    activators to them.
 * @param rewardProcessor Factory that produces new rooms by adding rewards to them.
 * @param attempts The number of times the algorithm starts over if it fails to produce a valid level.
 * @param pointlessAreaStrategyOpt The strategy to be used for pointless areas, or None if a default strategy should
 *                                 be used.
 * @param parameterResponseValidationOpt Validates if a set of accepting parameters are enough to approve a graph
 *                                       production compared to the rejecting set (if any), or None if the default
 *                                       validator should be used.
 * @param responderAmountEstimatorOpt Determines if a production modification should be kept based on two sets of
 *                                    accepting and rejecting parameters, or None if the default amount estimator
 *                                    should be used.
 * @tparam VType Type of room to be used in the level.
 * @tparam EType Type of corridor to be used in the level.
 */
class LevelStrategyBuilder[VType <: Room[VType]: ClassTag : TypeTag, EType[X] <: UnDiEdge[X] with DGSFunctionality[X, EType] with OuterEdge[X, EType]] private
  (edgeCopyFactory: EdgeCopyFactory[VType, EType],
   random: Random,
   activatorResponderProcessor: ActivatorResponderProcessor[VType, EType],
   rewardProcessor: RewardProcessor[VType, EType],
   attempts: Int,
   pointlessAreaStrategyOpt: Option[PointlessAreaReductionStrategy],
   parameterResponseValidationOpt: Option[ParameterResponderValidation[VType, EType]],
   responderAmountEstimatorOpt: Option[ActivatorResponderAmountEstimator[VType, EType]]) {

  if (attempts < 1)
    throw new IllegalArgumentException("Cannot instantiate a level strategy builder with less than 1 algorithm attempt (currently " + attempts + ").")

  private implicit val bindingModule = ProjectConfiguration

  private val pointlessAreaStrategy = pointlessAreaStrategyOpt.getOrElse { new PointlessAreaStrategy(random) }
  //TODO Add 1% accept ratio to documentation and the fact that the ParameterAcceptRatio class can be supplied by the user too
  private val parameterResponseValidation = parameterResponseValidationOpt.getOrElse { new ParameterAcceptRatio[VType, EType](0.01) }

  // Allows as many responders as there are rooms + corridors on a map.
  private val responderAmountEstimator = responderAmountEstimatorOpt.getOrElse { new DefaultActivatorResponderEstimator[VType, EType]() }

  /**
   * Instantiates a new level strategy builder with the minimum amount of user-supplied helper objects.
   * Along with this builder will be the default pointless area strategy (see documentation), a parameter response
   * validator that accepts any production where the number of approving parameters exceed the rejecting ones by 1% and
   * a responder amount estimator that lets the pointless area strategy add as many responders as needed.
   *
   * @param edgeCopyFactory Replaces the vertices in an edge, preserving the data in the edge itself. If an edge contains
   *                        data relative to the vertices connected to it, that data may be updated to reflect the new
   *                        vertex pair instead.
   * @param random Random object in charge of randomization for non-user supplied operations.
   * @param activatorResponderProcessor Adds an activator to a room and corresponding responder to an edge.
   * @param rewardProcessor Adds a reward to a room.
   * @param attempts If the final graph is not accepted in either the topology or object creation, the algorithm
   *                 starts over. This number controls how many times that is allowed to happen.
   */
  def this(edgeCopyFactory: EdgeCopyFactory[VType, EType],
           random: Random,
           activatorResponderProcessor: ActivatorResponderProcessor[VType, EType],
           rewardProcessor: RewardProcessor[VType, EType],
           attempts: Int) = {
    this(edgeCopyFactory, random, activatorResponderProcessor, rewardProcessor, attempts, None, None, None)
  }

  /**
   * Instantiates a new level creator.
   *
   * @param topologyParameters All parameters evaluating the topology phase of the level algorithm.
   * @param objectParameters All parameters evaluating the object placement phase of the level algorithm.
   * @param topologyProductions Tuples of all productions used to create the graph topology, as well as a value
   *                            representing how likely a production is to be selected at random. A production
   *                            with value 2.0 will be twice as likely to be chosen as one with 1.0. Probability values
   *                            must be higher than 0.
   * @param objectProductions Same as topologyProductions, but productions for the object placement phase.
   * @return A level creation strategy based on the parameter and production sets specified.
   */
  //TODO Change it so that you can supply a list of objects containing parameter/production sets in the order they should be processed. The pointless area strategy should also use the same interface and be contained in the list.
  def createLevelStrategy(topologyParameters: Set[Parameter[VType, EType]],
                         objectParameters: Set[Parameter[VType, EType]],
                         topologyProductions: Set[(Double, GraphProduction[VType, EType])],
                         objectProductions: Set[(Double, GraphProduction[VType, EType])]): Strategy[VType, EType] = {
    productionsAndParametersValidate(topologyParameters, objectParameters, topologyProductions, objectProductions)
    val topologyProductionCollection = new ProbabilityCollection[GraphProduction[VType, EType]](random)
    val objectProductionCollection = new ProbabilityCollection[GraphProduction[VType, EType]](random)

    for (kv <- topologyProductions) topologyProductionCollection.add(kv._1, kv._2)
    for (kv <- objectProductions) objectProductionCollection.add(kv._1, kv._2)

    val topologyStrategy = new HillClimbing[VType, EType](topologyParameters, topologyProductionCollection, parameterResponseValidation, attempts)
    val objectStrategy = if (!objectProductions.isEmpty) {
      new HillClimbing[VType, EType](objectParameters, objectProductionCollection, parameterResponseValidation, attempts)
    } else {
      new DummyStrategy[VType, EType]()
    }

    val levelStrategy = new LevelStrategy[VType, EType](topologyStrategy,
                                                        objectStrategy,
                                                        pointlessAreaStrategy,
                                                        edgeCopyFactory,
                                                        activatorResponderProcessor,
                                                        rewardProcessor,
                                                        responderAmountEstimator)
    levelStrategy
  }

  /**
   * Adds a custom pointless area strategy to a builder.
   * @param s Pointless area strategy to use.
   * @return A copy of this builder with the new strategy added.
   */
  def withPointlessAreaStrategy(s: PointlessAreaReductionStrategy): LevelStrategyBuilder[VType, EType] = {
    new LevelStrategyBuilder(edgeCopyFactory, random, activatorResponderProcessor, rewardProcessor, attempts, Option(s), Option(parameterResponseValidation), Option(responderAmountEstimator))
  }

  /**
   * Lets the user specify whether a pair of parameter sets representing the parameters that approve and rejects
   * a production means the productions modification should be kept or discarded.
   * @param v Validator to use.
   * @return A copy of this builder with the new validator added.
   */
  def withParameterValidation(v: ParameterResponderValidation[VType, EType]): LevelStrategyBuilder[VType, EType] = {
    new LevelStrategyBuilder(edgeCopyFactory, random, activatorResponderProcessor, rewardProcessor, attempts, Option(pointlessAreaStrategy), Option(v), Option(responderAmountEstimator))
  }

  /**
   * Lets the user decide the maximum amount of responders to be placed on a map by specifying a custom responder amount
   * estimator.
   * @param e Estimator to use.
   * @return A copy of this builder with the new estimator added.
   */
  def withResponderAmountEstimator(e: ActivatorResponderAmountEstimator[VType, EType]): LevelStrategyBuilder[VType, EType] = {
    new LevelStrategyBuilder(edgeCopyFactory, random, activatorResponderProcessor, rewardProcessor, attempts, Option(pointlessAreaStrategy), Option(parameterResponseValidation), Option(e))
  }

  private def productionsAndParametersValidate(topologyParameters: Set[Parameter[VType, EType]],
                                               objectParameters: Set[Parameter[VType, EType]],
                                               topologyProductions: Set[(Double, GraphProduction[VType, EType])],
                                               objectProductions: Set[(Double, GraphProduction[VType, EType])]): Boolean = {
    if (!topologyProductions.isEmpty && topologyParameters.isEmpty)
      throw new IllegalArgumentException("Cannot build level strategy: A set of topology productions were specified without topology parameters.")
    else if (topologyProductions.isEmpty && !topologyParameters.isEmpty)
      throw new IllegalArgumentException("Cannot build level strategy: A set of topology parameters were specified without topology productions.")
    else if (!objectProductions.isEmpty && objectParameters.isEmpty)
      throw new IllegalArgumentException("Cannot build level strategy: A set of object productions were specified without object parameters.")
    else if (objectProductions.isEmpty && !objectParameters.isEmpty)
      throw new IllegalArgumentException("Cannot build level strategy: A set of object parameters were specified without object productions.")
    else if (topologyProductions.isEmpty && topologyParameters.isEmpty)
      throw new IllegalArgumentException("Cannot build level strategy: Although it is possible to exclude object placement, the strategy must include topology productions and parameters.")

    true

  }

}
